Skip to content

Lottie

Lottie 对 CPU 和 内存有真很大的性能挑战,在使用的时候,需要关注性能问题 本文从 资源加载、资源本身、渲染层面进行探讨 Lottie 使用方式

一、加载

1.1 异步

Lottie 文件较大,按需加载时,需要以异步的方式分包打包,否则将会造成 js 文件过大,影响每个页面

js
 const loadLoop = import(
      /* webpackPreload: true */
      /* webpackChunkName: "lottie-[request]-loop" */
      `./assets/lottie-path/${this.customAnimationFile}/loop.json`);

1.2 同步

每次都需要加载 Lottie,给资源设置预加载 prefetch, preload 不建议打包到 js 文件中,否则 cdn 文件缓存会变更

js
import fail from './assets/fail.json';

1.3 并行、串行

多个 Lottie 需要一起出现时, 并行加载 先后出现时,串行按条件加载

js
 const loadLoop = import(
      /* webpackPreload: true */
      /* webpackChunkName: "lottie-[request]-loop" */
      `./assets/lottie-path/${this.customAnimationFile}/loop.json`);
 
 const loadShow = import(
      /* webpackPreload: true */
      /* webpackChunkName: "lottie-[request]-show" */
      `./assets/lottie-path/${this.customAnimationFile}/shoow.json`);

Promise.all([loadLoop, loadShow]).then(() => {})

1.4 缓存

  • 频繁读取的 Lottie 资源增加一定大的缓存,避免频繁的网络请求
js
private async getLottieData() {
  const { id, url, cache } = this.options;
  if (LOTTIE_CACHE.has(id)) {
    const value = LOTTIE_CACHE.get(id);
    if (value) {
      return value;
    }
  }
  try {
    const { data: res = [] } = await axios.get(url, {
      timeout: 5000,
    });
    const data = res;
    if (cache && data) {
      LOTTIE_CACHE.set(id, data);
      if (LOTTIE_CACHE.size > CACHE_MAX) {
        const keys = [...LOTTIE_CACHE.keys()];
        const earliestKey = keys[0];
        LOTTIE_CACHE.delete(earliestKey);
      }
    }
    return data;
  } catch (e) {
    error(loggerModules.pages, '[BaseEmoticon] 热词聊天', e);
  }
  return '';
}
  • 通过网络加载动态 Lottie 时,需要进行并发量作下控制
js
// 并行同时加载表情包
const ASYNC_POOL_NUM = 3;

asyncPool(ASYNC_POOL_NUM, visible, async (index: any) => {
  this.loadLottieAnimation(index, config);
});
export async function asyncPool(poolLimit: number, array: any, iteratorFn: any) {
  let ret: any = []; // 存储所有的异步任务
  let executing: any = []; // 存储所有正在执行的任务
  for (const item of array) {
    const p = Promise.resolve().then(() => iteratorFn(item, array));
    // 调用iteratorFn函数创建异步任务
    ret.push(p);
    // 保存新的异步任务

    if (poolLimit <= array.length) {
      // 当poolLimit小于等于总任务数量时,进行并发控制
      const e: any = p.then(() => executing.splice(executing.indexOf(e), 1));
      // 当任务完成后,从正在执行的任务队列中移除任务,腾出一个空位
      executing.push(e);
      // 加入正在执行的异步任务

      if (executing.length >= poolLimit) {
        await Promise.race(executing);
        // 有任务执行完成之后,进入下一次循环
      }
    }
  }
  return Promise.all(ret); // 所有任务完成之后返回
}

二、资源

2.1 大小

资源大小需要进行压缩 https://maotianyuan.github.io/ailer/packages/tiny/doc/ 该工具主要对 Lottie 图片进行压缩

2.2 外挂

离线本地的 Lottie 资源,且图片较丰富的 Lottie, 图片以外挂形式加载

js
export const getSkinLottie = async (path: string) => {
  try {
    console.log('[getSkinLottie] path = ', path);
    const { content = '' } = await EV2Client.invokeMethod.fileReader(`${path}/index.json`,{});
    if (!content) {
      return;
    }
    const loadLocalImg = (assetData: any) => {
      const data = JSON.parse(assetData);
      if (!Reflect.has(data, 'assets')) {
        return;
      }
      data.assets.forEach((i: any) => {
        if (i.u) {
          i.p = `file://${path}/${i.u}${i.p}`;
          i.u = '';
        }
      });
      return data;
    };
    return loadLocalImg(content);
  } catch (error) {
    console.error('getSkinLottie', error);
  }
  return;
};

2.3 base64 图片

网络加载 Lottie 资源,图片内置到 base64 否则网络加载图片不一致,部分图片展示时机无法像预想一样

三、渲染

动态修改

部分复杂动画效果,跟 Lottie 元素对齐的动态数据,可通过 Lottie 修改数据方式修改

js
anim.addEventListener('DOMLoaded', () => {
  /** 修改文字 */
  anim.renderer.elements[0].elements[0].updateDocumentData({t:'替换文本'},0); 

  /** 修改图片 */
  if (anim.renderer.rendererType === 'canvas') {
    // canvas 模式下的图片替换
    anim.renderer.elements[0].elements[8].img.src ='https://gw.alipayobjects.com/mdn/rms_91e1e4/afts/img/A*2mfsTo-gbDgAAAAAAAAAAABkARQnAQ';
  } else {
    // svg 模式下的图片替换,前两个参数为固定值
    anim.renderer.elements[0].elements[8].innerElem.setAttributeNS(
      'http://www.w3.org/1999/xlink',
      'href', 
      'https://gw.alipayobjects.com/mdn/rms_91e1e4/afts/img/A*2mfsTo-gbDgAAAAAAAAAAABkARQnAQ'
    );
  }  
});

css 实现成本不高的动画,使用 css 动画,避免 Lottie 层级过多

渲染时机

列表多 Lottie 动画,需要进行可视区渲染的控制,可视区外销毁, 防止 CPU 和 内存过高带来性能问题

js
  private async onObserver(config?: any) {
    try {
      const list = document.querySelectorAll('.base-emoticon');
      const getEmoticonStatus = async () => {
        let visible: any = [];
        let hidden: any = [];
        try {
          for (const element of list) {
            const id = element.getAttribute('data-id');
            const isVisible = await isInViewport(element);
            isVisible ? visible.push(id) : hidden.push(id);
          }
          const min = parseInt(visible[0], 10);
          const max = parseInt(visible[visible.length - 1], 10);

          // 找到比 visible 中最小值小的两个值,加入到 visible 数组开头
          const lowerTargets = hidden.filter((item: string) => parseInt(item, 10) < min).slice(REMAIN * -1);
          visible.unshift(...lowerTargets);

          // 找到比 visible 中最大值大的两个值,加入到 visible 数组末尾
          const upperTargets = hidden.filter((item: string) => parseInt(item, 10) > max).slice(0, REMAIN);
          visible.push(...upperTargets);

          hidden = hidden.filter((id: any) => !visible.includes(id));
          visible = visible.filter((item: any) => item !== undefined);

        } catch (error) {
          console.error('Error in getEmoticonStatus:', error);
        }
        return {
          visible,
          hidden,
        };
      };
      const { visible, hidden } = await getEmoticonStatus();
      asyncPool(ASYNC_POOL_NUM, visible, async (index: any) => {
        this.loadLottieAnimation(index, config);
      });
      hidden.forEach((item: any) => {
        this.desctroyLottie(item);
      });
    } catch (error) {
      console.error('Error in onObserver:', error);
    }
  }
  export const isInViewport = (element: any) => {
    return new Promise((resolve) => {
      const observer = new IntersectionObserver((entries) => {
        const isIntersecting = entries.some((entry) => entry.intersectionRatio > 0);
        resolve(isIntersecting);
        observer.unobserve(element);
        observer.disconnect();
      }, {
        rootMargin: '0px',
        threshold: 0,
      });
      observer.observe(element);
    });
  };

supported 跨端

动效设计时候,会出现 PC 无问题但 Android 和 iOS 异常,或者 PC 有问题但移动端没问题情况,需动效老师根据各端支持 supported 属性进行设计

在 MIT 许可下发布